package com.koi.system.service.login;

import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.excel.util.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.koi.common.constants.KoiCacheConstants;
import com.koi.common.constants.KoiConstants;
import com.koi.common.constants.SaTokenSessionConstants;
import com.koi.common.enums.log.ClientType;
import com.koi.common.ex.koi.YUtil;
import com.koi.common.redis.RedisUtil;
import com.koi.common.satoken.KoiLoginUser;
import com.koi.common.satoken.KoiLoginUtil;
import com.koi.common.satoken.KoiPasswordUtil;
import com.koi.common.utils.ip.IpAddressUtil;
import com.koi.common.utils.ip.IpUtil;
import com.koi.common.utils.ip.ServletUtil;
import com.koi.common.utils.number.VerifyCodeUtil;
import com.koi.common.utils.pwd.AESUtil;
import com.koi.model.bo.system.SysLoginBo;
import com.koi.model.vo.system.SysLoginVo;
import com.koi.model.vo.system.SysRegisterVo;
import com.koi.pojo.system.SysDept;
import com.koi.pojo.system.SysLoginUser;
import com.koi.system.service.dept.SysDeptService;
import com.koi.system.service.loginlog.SysLoginLogService;
import com.koi.system.service.role.SysRoleService;
import com.koi.system.service.user.SysLoginUserService;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import java.util.Date;

/**
 * @author ･ᴗ･
 * @description [AdminLoginServiceImpl]
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class SysLoginServiceImpl implements SysLoginService {

    private final RedisUtil redisUtil;

    private final IpAddressUtil ipAddressUtil;

    private final SysLoginUserService sysLoginUserService;

    private final SysLoginLogService sysLoginLogService;

    private final SysDeptService sysDeptService;

    private final SysRoleService sysRoleService;

    /**
     * @description [PC登录]
     * @author ･ᴗ･
     */
    @Override
    public SysLoginBo login(@Validated SysLoginVo loginVo) {
        YUtil.isTrue(ObjectUtils.isEmpty(loginVo), "登录失败");
        // 1、登录后，核实redis验证码数据
        String securityCode = redisUtil.getKey(KoiCacheConstants.CAPTCHA_CODE_KEY + loginVo.getCodeKey());
        YUtil.isTrue(StringUtils.isBlank(securityCode), "验证码已失效");
        log.info("登录账号：{}，验证码：{}", loginVo.getLoginName(), securityCode);
        // 2、前端登录输入验证码不区分大小写
        YUtil.isTrue(!securityCode.equalsIgnoreCase(loginVo.getSecurityCode()), "验证码输入错误");
        // 3、验证账号是否存在
        LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysLoginUser::getLoginName, loginVo.getLoginName());
        wrapper.last("LIMIT 1");
        // 4、根据登录名查询用户数据(失败，不抛出异常，影响登录日志记录)
        SysLoginUser loginUser = sysLoginUserService.getOne(wrapper, false);
        log.info("登录用户详细信息：{}", loginUser);
        // 5、用户不存在
        if (ObjectUtils.isEmpty(loginUser)) {
            // 保存登录日志(账户，1(失败)，提示信息，客户端Request)
            sysLoginLogService.addLoginLog(loginVo.getLoginName(), KoiConstants.ONE_STRING, "账户不存在");
            YUtil.isTrue("账户或密码不正确");
        }
        // 6、账户是否被冻结
        if (loginUser.getUserStatus().equals("1")) {
            // 保存登录日志
            sysLoginLogService.addLoginLog(loginVo.getLoginName(), KoiConstants.ONE_STRING, "账户已被冻结");
            YUtil.isTrue("账户已被冻结");
        }
        // 7、账户是否输入错误超过指定次数
        if (getPwdErrorNumber(loginVo.getLoginName()) >= 3) {
            // 保存登录日志
            sysLoginLogService.addLoginLog(loginVo.getLoginName(), KoiConstants.ONE_STRING, "十分钟内账号密码输错3次");
            // 账号锁定过期时间（秒）
            long expireTime = redisUtil.getExpire(KoiCacheConstants.PWD_ERROR_KEY + loginVo.getLoginName());
            YUtil.isTrue("账号密码输错3次，请" + expireTime + "秒后重试");
        }
        // 8、密码不正确进行处理
        if (!KoiPasswordUtil.getPassword(AESUtil.decrypt(loginVo.getPassword(), KoiConstants.AES_PWD_SECRET_KEY), loginUser.getSalt()).equals(loginUser.getPassword())) {
            // 保存登录日志
            sysLoginLogService.addLoginLog(loginVo.getLoginName(), KoiConstants.ONE_STRING, "密码不正确");
            // 仅仅配置十分钟内输错3次，若想输入5次进行锁定，请自行添加逻辑
            setPwdErrorExpire(loginVo.getLoginName());
            YUtil.isTrue("账户或密码不正确");
        }
        // 9、登录成功，保存登录日志[舍弃-Sa-Token监听其中进行保存登录日志也阔以]
        sysLoginLogService.addLoginLog(loginVo.getLoginName(), KoiConstants.ZERO_STRING, "登录成功");
        // 10、执行登录
        // 设置当前登录用户信息缓存，有需要的其他全局获取信息的，自行添加。例如：cityId、deptId、deptName等。
        KoiLoginUser koiLoginUser = setSaTokenLoginUserCache(loginUser);
        SaLoginModel model = new SaLoginModel();
        // 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在），当此值为false后，关闭浏览器后再次打开需要重新登录
        model.setIsLastingCookie(loginVo.getRememberMe());
        model.setDevice(KoiConstants.SYSTEM_PC);
        // 自定义分配不同用户体系的不同Token授权时间（不设置默认走全局 yml 配置）
        // 例如: 后台用户30分钟过期 app用户7天过期
        // model.setTimeout(30 * 60); // 指定此次登录token的有效期, 单位:秒 （如未指定，自动取全局配置的 timeout 值）
        // model.setExtra(); // 其他
        // 登录 并 生成token
        KoiLoginUtil.login(koiLoginUser, model);
        // 11、修改登录时间 和 登录IP地址
        LambdaUpdateWrapper<SysLoginUser> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(SysLoginUser::getLoginTime, new Date());
        updateWrapper.set(SysLoginUser::getLoginIp, IpUtil.getIpAddr(ServletUtil.getRequest()));
        updateWrapper.eq(SysLoginUser::getUserId, loginUser.getUserId());
        sysLoginUserService.update(null, updateWrapper);
        // 12、返回token
        SysLoginBo loginBo = new SysLoginBo();
        loginBo.setTokenName(StpUtil.getTokenName());
        loginBo.setTokenValue(StpUtil.getTokenValue());
        log.info("登录验证-当前会话是否登录：{}", StpUtil.isLogin());
        // 13、返回字段少，不用SysLoginBo实体类，直接使用 JSONObject 也是可以的
        // JSONObject jsonObject = new JSONObject();
        // jsonObject.put("tokenName", StpUtil.getTokenName());
        // jsonObject.put("tokenValue", StpUtil.getTokenValue());
        // 13、将用户数据放入缓存中，后续方便获取当前用户数据使用
        // 这个是以token当做key，存入redis中
        // StpUtil.getTokenSession().set(SaTokenSessionConstants.LOGIN_USER, koiLoginUser);
        // 这个是以sessionId[用户ID]当做key，存入redis中
        StpUtil.getSession().set(SaTokenSessionConstants.LOGIN_USER, koiLoginUser);
        // 14、登录成功，删除验证码
        redisUtil.deleteKey(KoiCacheConstants.CAPTCHA_CODE_KEY + loginVo.getCodeKey());
        // 15、登录成功，删除该用户的密码key
        clearPwdLock(loginVo.getLoginName());
        // 16、登录成功返回Token信息
        return loginBo;
    }

    /**
     * @description [设置PC端当前登录用户信息缓存]
     * @author ･ᴗ･
     */
    private KoiLoginUser setSaTokenLoginUserCache(SysLoginUser loginUser) {
        KoiLoginUser koiLoginUser = new KoiLoginUser();
        // 1、当前登录用户信息
        koiLoginUser.setUserId(loginUser.getUserId());
        koiLoginUser.setLoginName(loginUser.getLoginName());
        koiLoginUser.setUserName(loginUser.getUserName());
        koiLoginUser.setAvatar(loginUser.getAvatar());
        koiLoginUser.setSex(loginUser.getSex());
        koiLoginUser.setLoginTime(loginUser.getLoginTime());
        koiLoginUser.setPhone(loginUser.getPhone());
        koiLoginUser.setEmail(loginUser.getEmail());
        koiLoginUser.setUserType(loginUser.getUserType());
        koiLoginUser.setDeptId(loginUser.getDeptId());
        if (StringUtils.isNotBlank(loginUser.getDeptId())) {
            SysDept sysDept = sysDeptService.getById(loginUser.getDeptId());
            if (ObjectUtils.isNotEmpty(sysDept) && StringUtils.isNotBlank(sysDept.getDeptName())) {
                koiLoginUser.setDeptName(sysDept.getDeptName());
            }
        } else {
            koiLoginUser.setDeptName("无名部门");
        }
        koiLoginUser.setCreateTime(loginUser.getCreateTime());
        // 登录时，该用户必须拥有角色，该角色可以不分配部门权限数据
        boolean isKoiAdmin = sysRoleService.getIsKoiAdmin(koiLoginUser.getUserId());
        // 是否超级管理员
        koiLoginUser.setIsKoiAdmin(loginUser.getUserId().equals(KoiConstants.ONE_LONG) || isKoiAdmin);
        // 2、IP地址
        koiLoginUser.setIpAddress(loginUser.getLoginIp());
        // 3、登录地址
        koiLoginUser.setLoginAddress(ipAddressUtil.getAddress(loginUser.getLoginIp()));
        // 4、UserAgent信息
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtil.getRequest().getHeader("User-Agent"));
        // 5、登录浏览器
        koiLoginUser.setBrowser(userAgent.getBrowser().getName());
        // 6、登录操作系统
        koiLoginUser.setOs(userAgent.getOperatingSystem().getName());
        // 7、登录设备
        String deviceName = userAgent.getOperatingSystem().getDeviceType().getName();
        if (org.apache.commons.lang3.StringUtils.isBlank(deviceName)) {
            deviceName = "Default";
        } else if ("Computer".equals(deviceName)) {
            deviceName = ClientType.PC.name();
        } else if ("Mobile".equals(deviceName)) {
            deviceName = ClientType.MOBILE.name();
        }
        koiLoginUser.setDeviceName(deviceName);
        return koiLoginUser;
    }

    /**
     * @description [清除用户锁定状态]
     * @author ･ᴗ･
     */
    public void clearPwdLock(String loginName) {
        redisUtil.deleteKey(KoiCacheConstants.PWD_ERROR_KEY + loginName);
    }

    /**
     * @description [获取密码锁定次数]
     * @author ･ᴗ･
     */
    public int getPwdErrorNumber(String loginName) {
        Integer pwdErrorNumber = redisUtil.getKey(KoiCacheConstants.PWD_ERROR_KEY + loginName);
        return pwdErrorNumber == null ? 0 : pwdErrorNumber;
    }

    /**
     * @description [根据密码错误次数设置过期时间]
     * @author ･ᴗ･
     */
    public void setPwdErrorExpire(String loginName) {
        int pwdErrorNumber = getPwdErrorNumber(loginName);
        // 密码错误次数加一
        redisUtil.setCacheObjectMinutes(KoiCacheConstants.PWD_ERROR_KEY + loginName, pwdErrorNumber + 1, KoiConstants.TEN_NUMBER);
        // 十分钟内输错3次
        if (pwdErrorNumber >= 3) {
            redisUtil.expireMinutes(KoiCacheConstants.PWD_ERROR_KEY + loginName, KoiConstants.TEN_NUMBER);
            YUtil.isTrue("密码输入错误次数3次，锁定10分钟");
        }
    }

    /**
     * @description [退出]
     * @author ･ᴗ･
     */
    @Override
    public void logout() {
        StpUtil.logout();
    }

    /**
     * @description [PC注册]
     * @author ･ᴗ･
     */
    @Override
    public void register(SysRegisterVo registerVo) {
        // 1、检测邀请码是否存在 和 密码是否一致
        YUtil.isTrue(!registerVo.getRegCode().equals("KOI-ADMIN"), "邀请码输入错误");
        YUtil.isTrue(!registerVo.getRegPwd().equals(registerVo.getRegConfirmPwd()), "密码和确认密码不一致");
        // 2、验证账号是否存在
        LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysLoginUser::getLoginName, registerVo.getRegLoginName());
        wrapper.last("LIMIT 1");
        // 3、根据登录名查询用户数据
        SysLoginUser sysLoginUser = sysLoginUserService.getOne(wrapper);
        YUtil.isTrue(ObjectUtils.isNotEmpty(sysLoginUser), "当前登录名称已存在");
        // 4、新增注册用户，默认密码
        SysLoginUser loginUser = new SysLoginUser();
        loginUser.setLoginName(registerVo.getRegLoginName());
        loginUser.setSalt(VerifyCodeUtil.generateMixedLetters(6));
        loginUser.setPassword(KoiPasswordUtil.getPassword(registerVo.getRegPwd(), loginUser.getSalt()));
        loginUser.setUserType(KoiConstants.TWO_STRING);
        loginUser.setAvatar("https://pic1.zhimg.com/v2-60d16e77a47a49cfae8fb451ce514840_b.webp");
        boolean save = sysLoginUserService.save(loginUser);
        YUtil.isTrue(!save, "注册用户失败，请重试");
        // 5、自行配置默认权限菜单等[用户没有菜单，前端则会自动跳转登录页面，不会进入管理平台主页]。
    }

}
